/*
Package logger
author: jamon
time: 2023-08-15
desc:
*/
package logger

import (
	"fmt"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"
)

const (
	logDirName = "logs"
	logChanCap = 65536

	envLogConsoleLevel    = "TOG_LOG_CONSOLE_LEVEL"
	envLogFileLevel       = "TOG_LOG_FILE_LEVEL"
	envLogFileNameDefault = "TOG_LOG_FILE_NAME_DEFAULT"
	envLogFileMaxSize     = "TOG_LOG_FILE_MAX_SIZE"
	envLogFileBackupNum   = "TOG_LOG_FILE_BACKUP_NUM"
)

type Kwargs map[string]interface{}

var logConsoleLevel = zapcore.DebugLevel
var logFileLevel = zapcore.DebugLevel
var logFileNameDefault = "program.log"
var logFileMaxSize = 1024 // MB
var logFileBackupNum = 3

var logChan chan *logMessage
var logDoneChan chan int

var defaultLogger *zap.SugaredLogger

var globalLogger = NewEsLogger(nil) // 全局通用logger

type logMessage struct {
	level   zapcore.Level
	frame   runtime.Frame
	message string
	kvPairs Kwargs
}

func (msg *logMessage) String() string {
	// 获取当前的工作目录
	workingDir, err := os.Getwd()
	callerPath := msg.frame.File
	// 三方库路径处理
	callerPath, isThirdLib := convertPath(callerPath)

	if err == nil && !isThirdLib {
		// 计算相对路径
		relativePath, err := filepath.Rel(workingDir, msg.frame.File)
		if err == nil {
			callerPath = relativePath
		}
	}

	// 函数名不要包含路径
	lastSlashIndex := strings.LastIndex(msg.frame.Function, "/")
	shortFuncName := msg.frame.Function
	if lastSlashIndex >= 0 {
		shortFuncName = shortFuncName[lastSlashIndex+1:]
	}

	ret := fmt.Sprintf("%v|%v:%v|%v|%v", msg.level.String(),
		callerPath, msg.frame.Line, shortFuncName, msg.message)
	for k, v := range msg.kvPairs {
		ret += fmt.Sprintf("|%v=%v", k, v)
	}
	return ret
}

func InitConfig(logLevel string, serviceName string) {
	initEnvVars()

	logChan = make(chan *logMessage, logChanCap)
	logDoneChan = make(chan int)

	logDirPath := prepareLogDir()

	var defaultEncoder zapcore.Encoder

	defaultEncoder = zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
		TimeKey:  "T",
		LevelKey: "L",
		//CallerKey:   "C",
		MessageKey: "M",
		//FunctionKey: "F",
		EncodeTime: zapcore.ISO8601TimeEncoder,
		//EncodeLevel:      zapcore.CapitalLevelEncoder,
		//EncodeCaller:     zapcore.ShortCallerEncoder,
		ConsoleSeparator: "|",
	})

	logFileNameDefault = fmt.Sprintf("%v.log", serviceName)
	loglevel, err := zapcore.ParseLevel(logLevel)
	if err != nil {
		panic(fmt.Sprintf("failed to parse log level '%v': %v", logLevel, err))
	}
	logFileLevel = loglevel
	defaultLogger = newLogger(filepath.Join(logDirPath, logFileNameDefault), defaultEncoder)

	go runLoggerTask()
}

func initEnvVars() {
	var envVar string
	envVar = os.Getenv(envLogConsoleLevel)
	if len(envVar) > 0 {
		_ = logConsoleLevel.UnmarshalText([]byte(envVar))
	}
	envVar = os.Getenv(envLogFileLevel)
	if len(envVar) > 0 {
		_ = logFileLevel.UnmarshalText([]byte(envVar))
	}
	envVar = os.Getenv(envLogFileNameDefault)
	if len(envVar) > 0 {
		logFileNameDefault = envVar
	}
	envVar = os.Getenv(envLogFileMaxSize)
	if len(envVar) > 0 {
		if i, e := strconv.Atoi(envVar); e == nil && i > 0 {
			logFileMaxSize = i
		}
	}
	envVar = os.Getenv(envLogFileBackupNum)
	if len(envVar) > 0 {
		if i, e := strconv.Atoi(envVar); e == nil && i > 0 {
			logFileBackupNum = i
		}
	}
}

func prepareLogDir() string {
	err := os.MkdirAll(logDirName, os.ModePerm)
	if err != nil {
		panic(fmt.Sprintf("failed to prepare log dir path at '%v': %v", logDirName, err))
	}
	return logDirName
}

func newLogger(filePath string, encoder zapcore.Encoder) *zap.SugaredLogger {
	fileCore := zapcore.NewCore(encoder, rotateFileWriteSyncer(filePath), fileLevelEnabler{})
	consoleCore := zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), consoleLevelEnabler{})
	core := zapcore.NewTee(fileCore, consoleCore)
	log := zap.New(core, zap.AddCaller())
	return log.Sugar()
}

func rotateFileWriteSyncer(filename string) zapcore.WriteSyncer {
	return zapcore.AddSync(&lumberjack.Logger{
		Filename:   filename,
		MaxSize:    logFileMaxSize,
		MaxBackups: logFileBackupNum,
		LocalTime:  true,
	})
}

type fileLevelEnabler struct{}

func (fileLevelEnabler) Enabled(level zapcore.Level) bool {
	return level >= logFileLevel
}

type consoleLevelEnabler struct{}

func (consoleLevelEnabler) Enabled(level zapcore.Level) bool {
	return level >= logConsoleLevel
}

func logLevelEnabled(level zapcore.Level) bool {
	return level >= logFileLevel || level >= logConsoleLevel
}

func runLoggerTask() {
	for {
		msg := <-logChan
		if msg == nil {
			close(logDoneChan)
			return
		}
		formatMsg := msg.String()
		switch msg.level {
		case zapcore.DebugLevel:
			defaultLogger.Debug(formatMsg)
		case zapcore.InfoLevel:
			defaultLogger.Info(formatMsg)
		case zapcore.WarnLevel:
			defaultLogger.Warn(formatMsg)
		case zapcore.ErrorLevel:
			defaultLogger.Error(formatMsg)
		case zapcore.FatalLevel:
			defaultLogger.Fatal(formatMsg)
		}

	}
}

func Close() {
	logChan <- nil
	<-logDoneChan
}

type EsLogger struct {
	fixedEntries Kwargs
}

// NewEsLogger 新建一个EsLogger, 可选传入一个Kwargs设置日志中固定记录的键值对
func NewEsLogger(fixedEntries Kwargs) *EsLogger {
	return &EsLogger{
		fixedEntries: fixedEntries,
	}
}

// pathCache 用于缓存库的根路径到库名的映射
var pathCache = make(map[string]string)
var cacheLock = &sync.RWMutex{}

// convertPath 将绝对路径转换为"库名/库内相对路径文件名"的格式
func convertPath(absolutePath string) (string, bool) {
	cacheLock.RLock()
	for prefix, repoName := range pathCache {
		if strings.HasPrefix(absolutePath, prefix) {
			relativePath := strings.TrimPrefix(absolutePath, prefix)
			cacheLock.RUnlock()
			return fmt.Sprintf("@%s%s", repoName, relativePath), true
		}
	}
	cacheLock.RUnlock()

	parts := strings.Split(absolutePath, "/")
	for i, part := range parts {
		if strings.Contains(part, "@") {
			repoName := part[:strings.Index(part, "@")]
			relativePath := strings.Join(parts[i+1:], "/")
			rootPath := strings.Join(parts[:i+1], "/")

			cacheLock.Lock()
			pathCache[rootPath] = repoName
			cacheLock.Unlock()

			return fmt.Sprintf("@%s/%s", repoName, relativePath), true
		}
	}
	return absolutePath, false // 如果格式不匹配，返回原始路径
}

func GetCallerFrame(skip int) (frame runtime.Frame, ok bool) {
	const skipOffset = 2 // skip GetCallerFrame and Callers

	pc := make([]uintptr, 1)
	numFrames := runtime.Callers(skip+skipOffset, pc)
	if numFrames < 1 {
		return
	}

	frame, _ = runtime.CallersFrames(pc).Next()
	return frame, frame.PC != 0
}

func (l *EsLogger) log(level zapcore.Level, frameSkip int, msg string, kvPairs ...Kwargs) {
	if !logLevelEnabled(level) {
		return
	}

	frame, ok := GetCallerFrame(frameSkip + 1)
	if !ok {
		return
	}

	kvMap := make(Kwargs)
	for k, v := range l.fixedEntries {
		kvMap[k] = v
	}

	for _, kv := range kvPairs {
		for k, v := range kv {
			kvMap[k] = v
		}
	}

	logMsg := &logMessage{
		level:   level,
		frame:   frame,
		message: msg,
		kvPairs: kvMap,
	}

	logChan <- logMsg
	if level >= zapcore.FatalLevel {
		<-logDoneChan // 阻塞等待进程退出
	}
}

// Debug 按指定格式自动拼接(kwargs 为key value键值map, 别名Kwargs)
func (l *EsLogger) Debug(msg string, kwargs ...Kwargs) {
	l.log(zapcore.DebugLevel, 1, msg, kwargs...)
}

func Debug(msg string, kwargs ...Kwargs) {
	globalLogger.log(zapcore.DebugLevel, 1, msg, kwargs...)
}

// Info 按指定格式自动拼接(kwargs 为key value键值map, 别名Kwargs)
func (l *EsLogger) Info(msg string, kwargs ...Kwargs) {
	l.log(zapcore.InfoLevel, 1, msg, kwargs...)
}

func Info(msg string, kwargs ...Kwargs) {
	globalLogger.log(zapcore.InfoLevel, 1, msg, kwargs...)
}

// Warn 按指定格式自动拼接(kwargs 为key value键值map, 别名Kwargs)
func (l *EsLogger) Warn(msg string, kwargs ...Kwargs) {
	l.log(zapcore.WarnLevel, 1, msg, kwargs...)
}

func Warn(msg string, kwargs ...Kwargs) {
	globalLogger.log(zapcore.WarnLevel, 1, msg, kwargs...)
}

// Error 按指定格式自动拼接(kwargs 为key value键值map, 别名Kwargs)
func (l *EsLogger) Error(msg string, kwargs ...Kwargs) {
	l.log(zapcore.ErrorLevel, 1, msg, kwargs...)
}

func Error(msg string, kwargs ...Kwargs) {
	globalLogger.log(zapcore.ErrorLevel, 1, msg, kwargs...)
}
